import * as step from '@flow-step/step-toolkit'
import * as fs from 'fs'
import axios, {AxiosResponse} from 'axios'
import {UploadInputs} from './upload-inputs'
import * as https from 'https'
import * as crypto from 'crypto'

// 分块大小为100MB
const CHUNK_SIZE = 100 * 1024 * 1024

// 分块上传的文件大小阈值(800MB)
export const MULTIPART_UPLOAD_THRESHOLD = 800 * 1024 * 1024

/**
 * 格式化文件大小，自动选择合适的单位
 */
function formatFileSize(bytes: number): string {
    if (bytes === 0) return '0 B'
    
    const k = 1024
    const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
    const i = Math.floor(Math.log(bytes) / Math.log(k))
    
    if (i >= sizes.length) {
        return (bytes / Math.pow(k, sizes.length - 1)).toFixed(2) + ' ' + sizes[sizes.length - 1]
    }
    
    return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i]
}

/**
 * 启动分块上传会话
 */
async function startUpload(
    artifact: UploadInputs
): Promise<{uploadUuid: string; location: string}> {
    const agent = new https.Agent({
        rejectUnauthorized: false
    })

    const url = `${artifact.packageHost}/api/external/repo/${artifact.orgId}/GENERIC/repos/${artifact.repoId}/blobs/uploads`

    try {
        const response: AxiosResponse = await axios.post(url, null, {
            httpsAgent: agent,
            headers: {
                Authorization: artifact.credential,
                'Content-Length': '0'
            },
            maxRedirects: 0,
            validateStatus: (status) => status === 202 || status === 201
        })

        const location = response.headers['location'] as string
        const uploadUuid = response.headers['package-upload-uuid'] as string

        if (!location || !uploadUuid) {
            throw new Error(
                'Unable to get upload location, please check response headers location and package-upload-uuid'
            )
        }

        step.info(`Upload UUID: ${uploadUuid}`)
        step.info(`Upload URL: ${artifact.packageHost}${location}`)

        return {uploadUuid, location}
    } catch (error: any) {
        step.error('[ERROR] Failed to get multipart upload information')
        if (axios.isAxiosError(error) && error.response) {
            step.error(
                `Response status: ${error.response.status}, Response data: ${JSON.stringify(error.response.data)}`
            )
        }
        throw error
    }
}

/**
 * 获取当前已上传的偏移量
 */
async function getCurrentOffset(
    artifact: UploadInputs,
    location: string
): Promise<{currentStart: number; currentEnd: number}> {
    const agent = new https.Agent({
        rejectUnauthorized: false
    })

    try {
        const response: AxiosResponse = await axios.get(
            `${artifact.packageHost}${location}`,
            {
                httpsAgent: agent,
                headers: {
                    Authorization: artifact.credential
                }
            }
        )

        const rangeHeader = response.headers['range'] as string
        step.info(`Current range: ${rangeHeader}`)

        if (!rangeHeader) {
            return {currentStart: 0, currentEnd: 0}
        }

        // 解析 Range 头，格式为 "bytes=0-1048576"
        const match = rangeHeader.match(/bytes=(\d+)-(\d+)/)
        if (match) {
            const currentStart = parseInt(match[1], 10)
            const currentEnd = parseInt(match[2], 10)
            return {currentStart, currentEnd}
        }

        return {currentStart: 0, currentEnd: 0}
    } catch (error: any) {
        if (axios.isAxiosError(error) && error.response?.status === 404) {
            // 如果返回404，说明还没有上传任何内容
            return {currentStart: 0, currentEnd: 0}
        }
        throw error
    }
}

/**
 * 上传单个分块
 */
async function uploadChunk(
    artifact: UploadInputs,
    location: string,
    start: number,
    end: number,
    fileSize: number
): Promise<void> {
    const agent = new https.Agent({
        rejectUnauthorized: false
    })

    // 确保 end 不超过文件大小
    if (end > fileSize) {
        end = fileSize
    }

    step.info(`Uploading chunk: ${start}-${end}`)

    // 读取文件片段
    const stream = fs.createReadStream(artifact.artifactFullName, {
        start,
        end: end - 1 // createReadStream 的 end 是包含的，所以要减1
    })

    const contentLength = end - start

    try {
        await axios.patch(`${artifact.packageHost}${location}`, stream, {
            httpsAgent: agent,
            headers: {
                Authorization: artifact.credential,
                'Content-Type': 'application/octet-stream',
                'Content-Range': `${start}-${end}`,
                'Content-Length': contentLength.toString()
            },
            maxBodyLength: Infinity,
            maxContentLength: Infinity
        })

        step.info(`Chunk ${start}-${end} uploaded successfully.`)
    } catch (error: any) {
        step.error(`Failed to upload chunk ${start}-${end}`)
        if (axios.isAxiosError(error) && error.response) {
            step.error(
                `Response status: ${error.response.status}, Response data: ${JSON.stringify(error.response.data)}`
            )
        }
        throw error
    }
}

/**
 * 计算文件的 MD5
 */
function calculateMd5(filePath: string): Promise<string> {
    return new Promise((resolve, reject) => {
        const hash = crypto.createHash('md5')
        const stream = fs.createReadStream(filePath)

        stream.on('data', (data) => {
            hash.update(data)
        })

        stream.on('end', () => {
            resolve(hash.digest('hex'))
        })

        stream.on('error', (error) => {
            reject(error)
        })
    })
}

/**
 * 完成分块上传
 */
async function completeUpload(
    artifact: UploadInputs,
    location: string,
    digest: string
): Promise<{fileMd5: string}> {
    const agent = new https.Agent({
        rejectUnauthorized: false
    })

    try {
        const response: AxiosResponse = await axios.put(
            `${artifact.packageHost}${location}?digest=${digest}`,
            null,
            {
                httpsAgent: agent,
                headers: {
                    Authorization: artifact.credential,
                    'Content-Length': '0'
                }
            }
        )

        step.info('All chunks uploaded successfully!')
        step.info(`File digest: ${digest}`)

        const fileMd5 = response.headers['x-blob-checksum-md5'] as string
        if (!fileMd5) {
            throw new Error('Unable to get file MD5 from response headers')
        }

        return {fileMd5}
    } catch (error: any) {
        step.error('Failed to complete upload.')
        if (axios.isAxiosError(error) && error.response) {
            step.error(
                `Response status: ${error.response.status}, Response data: ${JSON.stringify(error.response.data)}`
            )
        }
        throw error
    }
}

/**
 * 创建制品记录
 */
async function createArtifactRecord(
    artifact: UploadInputs,
    fileMd5: string
): Promise<any> {
    const agent = new https.Agent({
        rejectUnauthorized: false
    })

    const url = `${artifact.packageHost}/api/protocol/${artifact.orgId}/GENERIC/${artifact.repoId}/files?version=${artifact.artifactVersion}&fileName=${artifact.artifact}`

    try {
        const response = await axios.post(url, null, {
            httpsAgent: agent,
            headers: {
                Authorization: artifact.credential,
                'x-artlab-checksum-upload': 'true',
                'x-artlab-checksum-md5': fileMd5
            }
        })

        return response.data
    } catch (error: any) {
        step.error('Failed to create artifact record.')
        if (axios.isAxiosError(error) && error.response) {
            step.error(
                `Response status: ${error.response.status}, Response data: ${JSON.stringify(error.response.data)}`
            )
        }
        throw error
    }
}

/**
 * 分块上传主函数
 */
export async function chunkUpload(artifact: UploadInputs): Promise<void> {
    if (artifact.credential === undefined) {
        step.error(
            'Failed to get packages user credential, please check if the service connection is invalid!'
        )
        throw new Error('packages user credential is empty')
    }

    const fileSize = fs.statSync(artifact.artifactFullName).size
    const fileSizeFormatted = formatFileSize(fileSize)

    step.info(`File size: ${fileSizeFormatted} (${fileSize} bytes)`)
    step.info(`File MD5: ${artifact.fileMD5}`)
    step.info('File size greater than 800MB, starting chunk upload')
    step.info(
        `Upload ${artifact.artifactFullName} to PACKAGES using chunk upload, artifact version is ${artifact.artifactVersion}`
    )

    // 1. 启动上传会话
    const {uploadUuid, location} = await startUpload(artifact)

    // 2. 循环上传所有分块
    let currentEnd = 0
    const totalChunks = Math.ceil(fileSize / CHUNK_SIZE)
    let uploadedChunks = 0

    while (currentEnd < fileSize) {
        // 获取当前已上传的偏移量
        const offset = await getCurrentOffset(artifact, location)
        currentEnd = offset.currentEnd

        // 计算下一个分块的范围
        const start = currentEnd
        const end = Math.min(start + CHUNK_SIZE, fileSize)

        if (start >= fileSize) {
            break
        }

        // 上传分块
        await uploadChunk(artifact, location, start, end, fileSize)
        currentEnd = end
        uploadedChunks++

        // 显示上传进度
        const progress = ((uploadedChunks / totalChunks) * 100).toFixed(2)
        step.info(`Upload progress: ${progress}% (${uploadedChunks}/${totalChunks})`)
    }

    // 3. 计算文件 MD5
    step.info('Calculating file MD5...')
    const md5Hash = await calculateMd5(artifact.artifactFullName)
    const digest = `md5:${md5Hash}`
    step.info(`File MD5: ${md5Hash}`)

    // 4. 完成上传
    const {fileMd5} = await completeUpload(artifact, location, digest)

    // 5. 创建制品记录
    const result = await createArtifactRecord(artifact, fileMd5)

    // 6. 验证 MD5
    if (fileMd5 !== artifact.fileMD5) {
        step.error(
            `Md5 not matched, source fileMd5: ${artifact.fileMD5}, upload fileMd5: ${fileMd5}`
        )
        throw new Error('Failed to upload artifact: MD5 mismatch')
    }

    // 7. 输出结果
    let packagePath = `repoId=${artifact.repoId}&artifactName=${artifact.artifact}&artifactVersion=${artifact.artifactVersion}`
    let artifactSource = 'system-packages'
    let jsonData = result

    if (!jsonData.successful) {
        step.error('Upload failed: ' + JSON.stringify(jsonData))
        throw new Error('Failed to upload artifact')
    }

    let output = `${artifact.stepIdentifier}.ARTIFACT={"ARTIFACT_URL":"${jsonData.object.url}","ARTIFACT_NAME":"${artifact.artifactName}","ARTIFACT_MD5":"${jsonData.object.fileMd5}","WEB_DOWNLOAD_URL":"${packagePath}","artifact":"${artifact.artifactName}","type":"packages","downloadUrl":"${jsonData.object.url}","md5":"${jsonData.object.fileMd5}","repoId":"${artifact.repoId}","version":"${artifact.artifactVersion}"}`
    let packageInfo = `PACKAGE_INFO={"packageLabel":"${artifact.artifact}","packagePath":"${artifact.packageHost}/generic/${artifact.repoId}/artifacts?artifactName=${artifact.artifact}&version=${artifact.artifactVersion}","uploadType":"${artifact.uploadType}","repoId":"${artifact.repoId}"}`
    let artifacts = `{"source":"${artifactSource}","type":"package","packageLabel":"${artifact.artifact}","objectKey":"${artifact.fileId}","bucketName":"${artifact.bucketName}","webDownloadURL":"${packagePath}","objectKeyPrefix":"${artifact.fileParentId}","name":"${artifact.artifact}","version":"${artifact.artifactVersion}","repoId":"${artifact.repoId}","fileSize":${jsonData.object.fileSize}}`
    let artifactsList = new Array()
    artifactsList.push(JSON.parse(artifacts))

    step.addOutput(output)
    step.addOutput(packageInfo)
    step.addOutput(`ARTIFACTS=` + JSON.stringify(artifactsList))
    step.info(`Artifact download URL is: ${jsonData.object.url}`)
    step.info('Chunk upload completed')

    // 8. 清理临时文件
    fs.unlinkSync(artifact.artifactFullName)
}

